动态代理说大不大,说小不小,可深可浅。往深了说还是对JVM的了解程度要足够深入,时间篇幅有限,本文专注于回答如下问题,不作更深入的探讨。
- JDK和Cglib动态代理,分别怎么使用
- JDK动态代理的原理
- Cglib动态代理的原理
- 为什么JDK动态代理一定要实现接口,而Cglib就不用?
- JDK和Cglib,本质上有什么区别?
JDK动态代理
使用
一个简单的场景
- 一个Service接口,拥有sayHello()方法
- 一个ServiceImpl实现类,实现Service
- 创建一个ServiceImpl的代理类,代理sayHello()方法,在调用原方法的前后,打印锚点
例子如下
1 | interface Service { |
总结一下,要点
- 被代理的类要实现接口
- 代理的逻辑要通过InvocationHandler实现
Proxy.newProxyInstance()
生成代理类,需要提供类加载器、被代理的接口、InvocationHandler
原理
源码自己跟,最终会来到核心方法
java.lang.reflect.Proxy.ProxyBuilder#defineProxyClass
关键逻辑:生成类的字节码流;使用sun.misc.Unsafe直接从流创建Class对象。
1
2
3
4
5
6
7
8
9
10
11
12
13private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) {
...
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
try {
Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile, 0, proxyClassFile.length, loader, null);
return pc;
} catch (ClassFormatError e) {
...
}
...
}java.lang.reflect.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int)
,这里可以看到这里是类字节码流生成的关键逻辑:凭空构建一个class流
- 常规class字节码文件的组成:魔数、版本号、常量池等
- 写入父类:固定为
"java/lang/reflect/Proxy"
- 写入需要被代理的接口,用户传入的
- 写入字段和方法,这其中包含了我们传入的InvocationHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout); // (write constant pool)
// u2 access_flags;
dout.writeShort(accessFlags);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));
// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}
// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}
// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}
// u2 attributes_count;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)
通过设置系统属性:System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true")
可以将生成的字节码保存为文件,然后反编译看结果
1 | package com.sun.proxy; |
几个要点
- 该代理类直接继承了Proxy类,实现类我们指定的Service接口
sayHello()
代理的原理:调用InvocationHandler.invoke()
完成实际调用- 代理类的所有方法,都会调用
InvocationHandler.invoke()
小结
JDK的动态代理,是从头构建新的类字节码流,然后加载到JVM中达成的。其使用方法必须依赖接口、InvocationHandler、Proxy,并不是非常方便。
CGlib
使用
类似上面,一个简单的场景
- 一个Service类,不用实现任何接口
- 创建Service的代理类,代理sayHello()方法,在调用原方法的前后,打印锚点
1 | open class PersonService { |
总结一下,要点
- 只需要被代理类自己,但被代理类和方法必须是open的,即可被继承和覆盖的
- 使用Enhancer类,方法拦截使用MethodInterceptor定义代理逻辑
原理
同样,跟跟源码,发现关键逻辑在:net.sf.cglib.proxy.Enhancer#generateClass
,这里是通过ASM库来生成类字节码的,过程比较复杂,需要对ASM API比较了解才能分析,这里这里暂时忽略。直接看生成的代码。
设置系统属性System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "xxx")
可以将生成的代理类输出。
1 | public class PersonService$$EnhancerByCGLIB$$470f9603 extends PersonService implements Factory { |
看到
- 生成的代理类直接继承了
Service
类 - 方法拦截都是通过
MethodInterceptor
- 构建
MethodProxy
传入,用于真实方法的调用。
注意:MethodInterceptor
中如果直接调用Method,会造成堆栈溢出。必须通过MethodProxy.invokeSuper()
方法调用才行。
小结
CGlib是基于ASM进行字节码生成的,在使用上会简单很多。
区别
可以看到,无论是JDK动态代理,还是CGlib,最终都是生成了代理类的字节码,并将其加载为新的类。从这个角度上看,貌似没啥区别呀?理论上,JDK的动态代理也可以设计成CGlib那样,直接基于类生成代理子类,就像有人做的那样。很多人说,JDK动态代理只能基于接口,是因为代理类继承了Proxy,而Java是单继承,没有办法再继承用户自定义类,我认为这个说法因果倒置了,都说了,如果想要继承自定义类,是能够办到的。对于这个问题,我的看法是JDK设计者故意为之,至于原因嘛,我也不大说得上来(说到底,还是菜)。
JDK代理和CGlib代理的区别,除了API使用上,更重要的是字节码生成方式上的区别:前者凭空生成;后者使用ASM基于被代理类生成。
都是生成,区别在于生成的效率以及生成的代理类的效率。这又涉及到谁效率高的问题了。用JMH大概试一试吧。我们用几乎一样的被代理类,生成代理类,调用方法。测试每个操作所耗费的时间。
1 | ///////////////////////CGlib的测试case |
对于上面的case,当只保留代理类创建逻辑时,测试结果
1 | Benchmark (count) Mode Cnt Score Error Units |
当同时保留创建和方法调用逻辑时,测试结果
1 | Benchmark (count) Mode Cnt Score Error Units |
当前这样的基准测试是不准确的,但还是大致可以得出代理类的创建CGlib比JDK快,但调用上JDK更快。快多少?快不了多少。
总结
按照本文的方式来探索动态代理,还是远远不够的,要想把这一块理解透彻,说到底,还是要对JVM有深入的研究,也就是说,还需要继续探索的点
ASM库的使用、深入理解
类加载的深入研究,ClassLoader类的剖析
sun.misc.Unsafe类的深入研究
JVM的深入研究